<script setup lang="ts">
import axios, { isAxiosError } from 'axios'
import type { AxiosProgressEvent } from 'axios'
import { computed, nextTick, onMounted, ref, watch } from 'vue'

type MessageRole = 'user' | 'assistant'

type Message = {
  id: string
  role: MessageRole
  content: string
  isStreaming?: boolean
}

type ResetOptions = {
  abort?: boolean
  markAssistantIdle?: boolean
}

const API_BASE = (import.meta.env.VITE_API_BASE ?? 'http://localhost:8081/api').replace(/\/$/, '')
const CHAT_ENDPOINT = `${API_BASE}/ai/chat`

const messages = ref<Message[]>([])
const sessionId = ref('')
const memoryId = ref<number>(0)
const inputValue = ref('')
const isStreaming = ref(false)
const error = ref<string | null>(null)
const chatEndRef = ref<HTMLDivElement | null>(null)
const controllerRef = ref<AbortController | null>(null)

const canSend = computed(() => inputValue.value.trim().length > 0 && !isStreaming.value)

const regenerateIdentifiers = () => {
  sessionId.value = crypto.randomUUID()
  const buffer = new Uint32Array(1)
  crypto.getRandomValues(buffer)
  memoryId.value = buffer[0] || Math.floor(Math.random() * 1_000_000_000)
}

onMounted(() => {
  regenerateIdentifiers()
})

watch(
  messages,
  async () => {
    await nextTick()
    chatEndRef.value?.scrollIntoView({ behavior: 'smooth' })
  },
  { deep: true },
)

const resetStreaming = (options: ResetOptions = {}) => {
  const { abort = true, markAssistantIdle = true } = options

  if (abort) {
    controllerRef.value?.abort()
  }
  controllerRef.value = null

  if (markAssistantIdle) {
    const next = [...messages.value]
    for (let index = next.length - 1; index >= 0; index -= 1) {
      const candidate = next[index]
      if (!candidate) {
        continue
      }
      if (candidate.role === 'assistant' && candidate.isStreaming) {
        next[index] = { ...candidate, isStreaming: false }
        break
      }
    }
    messages.value = next
  }

  isStreaming.value = false
}

const handleSubmit = async (event?: SubmitEvent) => {
  event?.preventDefault()
  const trimmed = inputValue.value.trim()
  if (!trimmed || !sessionId.value || isStreaming.value) {
    return
  }

  const timestamp = Date.now()
  const userMessage: Message = {
    id: `${timestamp}-user`,
    role: 'user',
    content: trimmed,
  }
  const assistantMessageId = `${timestamp}-assistant`
  const assistantMessage: Message = {
    id: assistantMessageId,
    role: 'assistant',
    content: '',
    isStreaming: true,
  }

  messages.value = [...messages.value, userMessage, assistantMessage]
  inputValue.value = ''
  error.value = null
  isStreaming.value = true

  const controller = new AbortController()
  controllerRef.value = controller

  let accumulatedContent = ''
  let buffer = ''
  let processedLength = 0
  let doneStreaming = false

  const processEvent = (rawEvent: string) => {
    if (!rawEvent) {
      return
    }
    const data = rawEvent
      .split('\n')
      .filter((line) => line.startsWith('data:'))
      .map((line) => line.replace(/^data:\s?/, '').trimEnd())
      .join('\n')

    if (!data) {
      return
    }

    if (data === '[DONE]') {
      doneStreaming = true
      return
    }

    accumulatedContent += data
    messages.value = messages.value.map((message) =>
      message.id === assistantMessageId
        ? {
            ...message,
            content: accumulatedContent,
            isStreaming: true,
          }
        : message,
    )
  }

  const flushBuffer = () => {
    let boundary = buffer.indexOf('\n\n')
    while (boundary !== -1) {
      const rawEvent = buffer.slice(0, boundary)
      buffer = buffer.slice(boundary + 2)
      processEvent(rawEvent)
      if (doneStreaming) {
        buffer = ''
        return
      }
      boundary = buffer.indexOf('\n\n')
    }
  }

  const handleChunk = (chunk: string) => {
    if (!chunk) {
      return
    }
    buffer += chunk
    flushBuffer()
  }

  try {
    await axios.get<string>(CHAT_ENDPOINT, {
      params: {
        memoryId: memoryId.value,
        message: trimmed,
      },
      headers: {
        Accept: 'text/event-stream',
      },
      responseType: 'text',
      signal: controller.signal,
      onDownloadProgress: (progressEvent: AxiosProgressEvent) => {
        const event = progressEvent.event
        const target = (event?.target ?? event?.currentTarget) as XMLHttpRequest | null
        if (!target) {
          return
        }
        const responseText = target.responseText ?? ''
        if (!responseText) {
          return
        }
        const chunk = responseText.substring(processedLength)
        if (!chunk) {
          return
        }
        processedLength = responseText.length
        handleChunk(chunk)
      },
    })

    if (!doneStreaming && buffer.length > 0) {
      flushBuffer()
    }

    messages.value = messages.value.map((message) =>
      message.id === assistantMessageId
        ? {
            ...message,
            content: accumulatedContent,
            isStreaming: false,
          }
        : message,
    )
  } catch (err) {
    if (axios.isCancel(err)) {
      error.value = null
      messages.value = messages.value.map((message) =>
        message.id === assistantMessageId
          ? {
              ...message,
              content: accumulatedContent,
              isStreaming: false,
            }
          : message,
      )
    } else if (isAxiosError(err)) {
      const description = err.response?.data?.message ?? err.message ?? '未知错误，请稍后再试。'
      error.value = description
      messages.value = messages.value.map((message) =>
        message.id === assistantMessageId
          ? {
              ...message,
              content: accumulatedContent || description,
              isStreaming: false,
            }
          : message,
      )
    } else if (err instanceof Error) {
      const description = err.message || '未知错误，请稍后再试。'
      error.value = description
      messages.value = messages.value.map((message) =>
        message.id === assistantMessageId
          ? {
              ...message,
              content: accumulatedContent || description,
              isStreaming: false,
            }
          : message,
      )
    } else {
      const description = '未知错误，请稍后再试。'
      error.value = description
      messages.value = messages.value.map((message) =>
        message.id === assistantMessageId
          ? {
              ...message,
              content: accumulatedContent || description,
              isStreaming: false,
            }
          : message,
      )
    }
  } finally {
    if (!doneStreaming) {
      messages.value = messages.value.map((message) =>
        message.id === assistantMessageId
          ? {
              ...message,
              content: accumulatedContent,
              isStreaming: false,
            }
          : message,
      )
    }
    resetStreaming({ abort: false, markAssistantIdle: false })
  }
}

const handleKeyDown = (event: KeyboardEvent) => {
  if (event.key === 'Enter' && !event.shiftKey) {
    event.preventDefault()
    void handleSubmit()
  }
}

const startNewChat = () => {
  resetStreaming()
  messages.value = []
  inputValue.value = ''
  error.value = null
  regenerateIdentifiers()
}
</script>

<template>
  <div class="app">
    <header class="app__header">
      <div>
        <h1 class="app__title">AI 编程小助手</h1>
        <p class="app__subtitle">解答学习疑问，准备面试挑战</p>
      </div>
      <div class="app__meta">
        <span class="app__session-label">会话 ID</span>
        <span class="app__session-id">{{ sessionId }}</span>
        <button class="app__reset" type="button" @click="startNewChat">新会话</button>
      </div>
    </header>

    <main class="chat">
      <section class="chat__history">
        <template v-if="messages.length === 0">
          <div class="chat__placeholder">
            <h2>欢迎使用 AI 编程小助手</h2>
            <p>提出你的编程问题，或输入面试题目，我们将实时给出建议。</p>
          </div>
        </template>
        <template v-else>
          <div
            v-for="message in messages"
            :key="message.id"
            :class="['message', 'message--' + message.role]"
          >
            <div class="message__avatar" aria-hidden="true">
              {{ message.role === 'assistant' ? 'AI' : '我' }}
            </div>
            <div class="message__bubble">
              <p>{{ message.content }}</p>
              <span v-if="message.isStreaming" class="message__cursor" />
            </div>
          </div>
        </template>
        <div ref="chatEndRef" />
      </section>

      <form class="composer" @submit="handleSubmit">
        <textarea
          class="composer__input"
          placeholder="输入你的问题，按 Enter 发送，Shift + Enter 换行"
          v-model="inputValue"
          @keydown="handleKeyDown"
          rows="3"
          :disabled="isStreaming"
        />
        <div class="composer__actions">
          <button
            v-if="isStreaming"
            class="composer__button composer__button--ghost"
            type="button"
            @click="resetStreaming()"
          >
            停止生成
          </button>
          <button class="composer__button" type="submit" :disabled="!canSend">
            发送
          </button>
        </div>
      </form>
      <div v-if="error" class="composer__error">{{ error }}</div>
    </main>
  </div>
</template>